Dowiedz si臋, jak efektywnie testowa膰 aplikacje FastAPI za pomoc膮 TestClient. Poznaj najlepsze praktyki, zaawansowane techniki i rzeczywiste przyk艂ady dla solidnych i niezawodnych API.
Mistrzowskie Testowanie FastAPI: Kompleksowy Przewodnik po TestClient
FastAPI sta艂 si臋 wiod膮cym frameworkiem do budowania wysokowydajnych API w Pythonie. Jego szybko艣膰, 艂atwo艣膰 u偶ycia i automatyczna walidacja danych czyni膮 go ulubie艅cem w艣r贸d programist贸w na ca艂ym 艣wiecie. Jednak dobrze zbudowane API jest tak dobre, jak jego testy. Dok艂adne testowanie zapewnia, 偶e Twoje API dzia艂a zgodnie z oczekiwaniami, pozostaje stabilne pod presj膮 i mo偶e by膰 z pewno艣ci膮 wdra偶ane na produkcj臋. Ten kompleksowy przewodnik koncentruje si臋 na u偶yciu TestClient FastAPI do skutecznego testowania punkt贸w ko艅cowych API.
Dlaczego Testowanie Jest Wa偶ne dla Aplikacji FastAPI?
Testowanie jest kluczowym krokiem w cyklu 偶ycia tworzenia oprogramowania. Pomaga:
- Wcze艣nie identyfikowa膰 b艂臋dy: Wy艂apuj b艂臋dy, zanim trafi膮 na produkcj臋, oszcz臋dzaj膮c czas i zasoby.
- Zapewni膰 jako艣膰 kodu: Promuj dobrze ustrukturyzowany i 艂atwy w utrzymaniu kod.
- Zapobiega膰 regresjom: Gwarantuj, 偶e nowe zmiany nie psuj膮 istniej膮cej funkcjonalno艣ci.
- Poprawi膰 niezawodno艣膰 API: Buduj zaufanie do stabilno艣ci i wydajno艣ci API.
- U艂atwi膰 wsp贸艂prac臋: Zapewnij jasn膮 dokumentacj臋 oczekiwanego zachowania dla innych programist贸w.
Wprowadzenie do TestClient FastAPI
FastAPI udost臋pnia wbudowany TestClient, kt贸ry upraszcza proces testowania punkt贸w ko艅cowych API. TestClient dzia艂a jak lekki klient, kt贸ry mo偶e wysy艂a膰 偶膮dania do API bez uruchamiania pe艂noprawnego serwera. To sprawia, 偶e testowanie jest znacznie szybsze i wygodniejsze.
Kluczowe Funkcje TestClient:
- Symuluje 偶膮dania HTTP: Pozwala wysy艂a膰 偶膮dania GET, POST, PUT, DELETE i inne 偶膮dania HTTP do API.
- Obs艂uguje serializacj臋 danych: Automatycznie serializuje dane 偶膮dania (np. 艂adunki JSON) i deserializuje dane odpowiedzi.
- Udost臋pnia metody asercji: Oferuje wygodne metody weryfikacji kodu statusu, nag艂贸wk贸w i zawarto艣ci odpowiedzi.
- Obs艂uguje testowanie asynchroniczne: Dzia艂a bezproblemowo z asynchroniczn膮 natur膮 FastAPI.
- Integruje si臋 z frameworkami testowymi: 艁atwo integruje si臋 z popularnymi frameworkami testowymi Pythona, takimi jak pytest i unittest.
Konfiguracja 艢rodowiska Testowego
Zanim zaczniesz testowa膰, musisz skonfigurowa膰 艣rodowisko testowe. Zazwyczaj obejmuje to zainstalowanie niezb臋dnych zale偶no艣ci i skonfigurowanie frameworka testowego.
Instalacja
Najpierw upewnij si臋, 偶e masz zainstalowane FastAPI i pytest. Mo偶esz je zainstalowa膰 za pomoc膮 pip:
pip install fastapi pytest httpx
httpx to klient HTTP, kt贸rego FastAPI u偶ywa "pod mask膮". Chocia偶 TestClient jest cz臋艣ci膮 FastAPI, posiadanie zainstalowanego httpx zapewnia p艂ynne testowanie. Niekt贸re samouczki wspominaj膮 r贸wnie偶 o requests, jednak httpx jest bardziej zgodny z asynchroniczn膮 natur膮 FastAPI.
Przyk艂adowa Aplikacja FastAPI
Stw贸rzmy prost膮 aplikacj臋 FastAPI, kt贸rej mo偶emy u偶y膰 do testowania:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.get("/")
async def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
@app.post("/items/")
async def create_item(item: Item):
return item
Zapisz ten kod jako main.py. Ta aplikacja definiuje trzy punkty ko艅cowe:
/: Prosty punkt ko艅cowy GET, kt贸ry zwraca komunikat "Hello World"./items/{item_id}: Punkt ko艅cowy GET, kt贸ry zwraca element na podstawie jego ID./items/: Punkt ko艅cowy POST, kt贸ry tworzy nowy element.
Pisanie Pierwszego Testu
Teraz, gdy masz aplikacj臋 FastAPI, mo偶esz zacz膮膰 pisa膰 testy za pomoc膮 TestClient. Utw贸rz nowy plik o nazwie test_main.py w tym samym katalogu co main.py.
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
W tym te艣cie:
- Importujemy
TestClienti instancj臋 FastAPIapp. - Tworzymy instancj臋
TestClient, przekazuj膮capp. - Definiujemy funkcj臋 testow膮
test_read_root. - Wewn膮trz funkcji testowej u偶ywamy
client.get("/"), aby wys艂a膰 偶膮danie GET do g艂贸wnego punktu ko艅cowego. - Sprawdzamy, czy kod statusu odpowiedzi to 200 (OK).
- Sprawdzamy, czy JSON odpowiedzi jest r贸wny
{"message": "Hello World"}.
Uruchamianie Test贸w za Pomoc膮 pytest
Aby uruchomi膰 testy, po prostu otw贸rz terminal w katalogu zawieraj膮cym plik test_main.py i uruchom nast臋puj膮ce polecenie:
pytest
pytest automatycznie wykryje i uruchomi wszystkie testy w Twoim projekcie. Powiniene艣 zobaczy膰 dane wyj艣ciowe podobne do tego:
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /path/to/your/project
collected 1 item
test_main.py .
============================== 1 passed in 0.01s ==============================
Testowanie R贸偶nych Metod HTTP
TestClient obs艂uguje wszystkie standardowe metody HTTP, w tym GET, POST, PUT, DELETE i PATCH. Zobaczmy, jak testowa膰 ka偶d膮 z tych metod.
Testowanie 呕膮da艅 GET
Widzieli艣my ju偶 przyk艂ad testowania 偶膮dania GET w poprzedniej sekcji. Oto kolejny przyk艂ad, testuj膮cy punkt ko艅cowy /items/{item_id}:
def test_read_item():
response = client.get("/items/1?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "q": "test"}
Ten test wysy艂a 偶膮danie GET do /items/1 z parametrem zapytania q=test. Nast臋pnie sprawdza, czy kod statusu odpowiedzi to 200 i czy JSON odpowiedzi zawiera oczekiwane dane.
Testowanie 呕膮da艅 POST
Aby przetestowa膰 偶膮danie POST, musisz wys艂a膰 dane w tre艣ci 偶膮dania. TestClient automatycznie serializuje dane do JSON.
def test_create_item():
item_data = {"name": "Example Item", "description": "A test item", "price": 9.99, "tax": 1.00}
response = client.post("/items/", json=item_data)
assert response.status_code == 200
assert response.json() == item_data
W tym te艣cie:
- Tworzymy s艂ownik
item_datazawieraj膮cy dane dla nowego elementu. - U偶ywamy
client.post("/items/", json=item_data), aby wys艂a膰 偶膮danie POST do punktu ko艅cowego/items/, przekazuj膮citem_datajako 艂adunek JSON. - Sprawdzamy, czy kod statusu odpowiedzi to 200 i czy JSON odpowiedzi pasuje do
item_data.
Testowanie 呕膮da艅 PUT, DELETE i PATCH
Testowanie 偶膮da艅 PUT, DELETE i PATCH jest podobne do testowania 偶膮da艅 POST. Po prostu u偶ywasz odpowiednich metod na TestClient:
def test_update_item():
item_data = {"name": "Updated Item", "description": "An updated test item", "price": 19.99, "tax": 2.00}
response = client.put("/items/1", json=item_data)
assert response.status_code == 200
# Add assertions for the expected response
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
# Add assertions for the expected response
def test_patch_item():
item_data = {"price": 29.99}
response = client.patch("/items/1", json=item_data)
assert response.status_code == 200
# Add assertions for the expected response
Pami臋taj, aby doda膰 asercje, aby zweryfikowa膰, czy odpowiedzi s膮 zgodne z oczekiwaniami.
Zaawansowane Techniki Testowania
TestClient oferuje kilka zaawansowanych funkcji, kt贸re mog膮 pom贸c w pisaniu bardziej kompleksowych i skutecznych test贸w.
Testowanie z Zale偶no艣ciami
System wstrzykiwania zale偶no艣ci FastAPI pozwala 艂atwo wstrzykiwa膰 zale偶no艣ci do punkt贸w ko艅cowych API. Podczas testowania mo偶esz chcie膰 zast膮pi膰 te zale偶no艣ci, aby zapewni膰 mockowe lub specyficzne dla test贸w implementacje.
Na przyk艂ad, za艂贸偶my, 偶e Twoja aplikacja zale偶y od po艂膮czenia z baz膮 danych. Mo偶esz zast膮pi膰 zale偶no艣膰 bazy danych w testach, aby u偶y膰 bazy danych w pami臋ci:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base, Session
# Database Configuration
DATABASE_URL = "sqlite:///./test.db" # In-memory database for testing
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Define User Model
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
password = Column(String)
Base.metadata.create_all(bind=engine)
# FastAPI App
app = FastAPI()
# Dependency to get the database session
def get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
# Endpoint to create a user
@app.post("/users/")
async def create_user(username: str, password: str, db: Session = Depends(get_db)):
db_user = User(username=username, password=password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
from fastapi.testclient import TestClient
from .main import app, get_db, Base, engine, TestingSessionLocal
client = TestClient(app)
# Override the database dependency for testing
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
def test_create_user():
# First, ensure the tables are created, which may not happen by default
Base.metadata.create_all(bind=engine) # important: create the tables in the test db
response = client.post("/users/", params={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert response.json()["username"] == "testuser"
# Clean up the override after the test if needed
app.dependency_overrides = {}
Ten przyk艂ad zast臋puje zale偶no艣膰 get_db funkcj膮 specyficzn膮 dla test贸w, kt贸ra zwraca sesj臋 do bazy danych SQLite w pami臋ci. Wa偶ne: Tworzenie metadanych musi by膰 jawnie wywo艂ane, aby testowa baza danych dzia艂a艂a poprawnie. Brak utworzenia tabeli spowoduje b艂臋dy zwi膮zane z brakuj膮cymi tabelami.
Testowanie Kodu Asynchronicznego
FastAPI jest zbudowane jako asynchroniczne, wi臋c cz臋sto b臋dziesz musia艂 testowa膰 kod asynchroniczny. TestClient bezproblemowo obs艂uguje testowanie asynchroniczne.
Aby przetestowa膰 asynchroniczny punkt ko艅cowy, po prostu zdefiniuj funkcj臋 testow膮 jako async:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(0.1) # Simulate some async operation
return {"message": "Async Hello"}
import pytest
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
@pytest.mark.asyncio # Needed to be compatible with pytest-asyncio
async def test_async_endpoint():
response = client.get("/async")
assert response.status_code == 200
assert response.json() == {"message": "Async Hello"}
Uwaga: Musisz zainstalowa膰 pytest-asyncio, aby u偶ywa膰 @pytest.mark.asyncio: pip install pytest-asyncio. Musisz r贸wnie偶 upewni膰 si臋, 偶e asyncio.get_event_loop() jest skonfigurowane, je艣li u偶ywasz starszych wersji pytest. Je艣li u偶ywasz pytest w wersji 8 lub nowszej, mo偶e to nie by膰 wymagane.
Testowanie Przesy艂ania Plik贸w
FastAPI u艂atwia obs艂ug臋 przesy艂ania plik贸w. Aby przetestowa膰 przesy艂anie plik贸w, mo偶esz u偶y膰 parametru files metod 偶膮da艅 TestClient.
from fastapi import FastAPI, File, UploadFile
from typing import List
app = FastAPI()
@app.post("/files/")
async def create_files(files: List[bytes] = File()):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile]):
return {"filenames": [file.filename for file in files]}
from fastapi.testclient import TestClient
from .main import app
import io
client = TestClient(app)
def test_create_files():
file_content = b"Test file content"
files = [('files', ('test.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/files/", files=files)
assert response.status_code == 200
assert response.json() == {"file_sizes": [len(file_content)]}
def test_create_upload_files():
file_content = b"Test upload file content"
files = [('files', ('test_upload.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/uploadfiles/", files=files)
assert response.status_code == 200
assert response.json() == {"filenames": ["test_upload.txt"]}
W tym te艣cie tworzymy fikcyjny plik za pomoc膮 io.BytesIO i przekazujemy go do parametru files. Parametr files akceptuje list臋 krotek, gdzie ka偶da krotka zawiera nazw臋 pola, nazw臋 pliku i zawarto艣膰 pliku. Typ zawarto艣ci jest wa偶ny dla poprawnej obs艂ugi przez serwer.
Testowanie Obs艂ugi B艂臋d贸w
Wa偶ne jest, aby przetestowa膰, jak Twoje API obs艂uguje b艂臋dy. Mo偶esz u偶y膰 TestClient, aby wysy艂a膰 nieprawid艂owe 偶膮dania i sprawdzi膰, czy API zwraca poprawne odpowiedzi o b艂臋dach.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id > 100:
raise HTTPException(status_code=400, detail="Item ID too large")
return {"item_id": item_id}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_item_error():
response = client.get("/items/101")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID too large"}
Ten test wysy艂a 偶膮danie GET do /items/101, kt贸re zg艂asza HTTPException z kodem statusu 400. Test sprawdza, czy kod statusu odpowiedzi to 400 i czy JSON odpowiedzi zawiera oczekiwany komunikat o b艂臋dzie.
Testowanie Funkcji Bezpiecze艅stwa
Je艣li Twoje API u偶ywa uwierzytelniania lub autoryzacji, b臋dziesz musia艂 r贸wnie偶 przetestowa膰 te funkcje bezpiecze艅stwa. TestClient pozwala ustawia膰 nag艂贸wki i pliki cookie, aby symulowa膰 uwierzytelnione 偶膮dania.
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app = FastAPI()
# Security
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Simulate authentication
if form_data.username != "testuser" or form_data.password != "password123":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
return {"access_token": "fake_token", "token_type": "bearer"}
@app.get("/protected")
async def protected_route(token: str = Depends(oauth2_scheme)):
return {"message": "Protected data"}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_login():
response = client.post("/token", data={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert "access_token" in response.json()
def test_protected_route():
# First, get a token
token_response = client.post("/token", data={"username": "testuser", "password": "password123"})
token = token_response.json()["access_token"]
# Then, use the token to access the protected route
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) # corrected format.
assert response.status_code == 200
assert response.json() == {"message": "Protected data"}
W tym przyk艂adzie testujemy punkt ko艅cowy logowania, a nast臋pnie u偶ywamy 锌芯谢褍褔械薪薪褘泄 token do uzyskania dost臋pu do chronionej trasy. Parametr headers metod 偶膮da艅 TestClient pozwala ustawia膰 niestandardowe nag艂贸wki, w tym nag艂贸wek Authorization dla token贸w okaziciela.
Najlepsze Praktyki Testowania FastAPI
Oto kilka najlepszych praktyk, kt贸rych nale偶y przestrzega膰 podczas testowania aplikacji FastAPI:
- Pisz kompleksowe testy: D膮偶 do wysokiego pokrycia testami, aby zapewni膰 dok艂adne przetestowanie wszystkich cz臋艣ci API.
- U偶ywaj opisowych nazw test贸w: Upewnij si臋, 偶e nazwy test贸w wyra藕nie wskazuj膮, co test weryfikuje.
- Post臋puj zgodnie ze wzorcem Arrange-Act-Assert: Zorganizuj swoje testy w trzy odr臋bne fazy: Arrange (przygotuj dane testowe), Act (wykonaj testowan膮 akcj臋) i Assert (zweryfikuj wyniki).
- U偶ywaj obiekt贸w mock: Mockuj zewn臋trzne zale偶no艣ci, aby odizolowa膰 testy i unikn膮膰 polegania na zewn臋trznych systemach.
- Testuj przypadki brzegowe: Testuj swoje API z nieprawid艂owymi lub nieoczekiwanymi danymi wej艣ciowymi, aby upewni膰 si臋, 偶e obs艂uguje b艂臋dy w elegancki spos贸b.
- Uruchamiaj testy cz臋sto: Zintegruj testowanie ze swoim przep艂ywem pracy programistycznej, aby wcze艣nie i cz臋sto wychwytywa膰 b艂臋dy.
- Integruj z CI/CD: Zautomatyzuj swoje testy w potoku CI/CD, aby zapewni膰, 偶e wszystkie zmiany kodu s膮 dok艂adnie testowane przed wdro偶eniem na produkcj臋. Narz臋dzia takie jak Jenkins, GitLab CI, GitHub Actions lub CircleCI mog膮 by膰 u偶ywane do osi膮gni臋cia tego celu.
Przyk艂ad: Testowanie Internacjonalizacji (i18n)
Podczas tworzenia API dla globalnej publiczno艣ci kluczowa jest internacjonalizacja (i18n). Testowanie i18n obejmuje weryfikacj臋, czy Twoje API poprawnie obs艂uguje wiele j臋zyk贸w i region贸w. Oto przyk艂ad, jak mo偶esz testowa膰 i18n w aplikacji FastAPI:
from fastapi import FastAPI, Header
from typing import Optional
app = FastAPI()
messages = {
"en": {"greeting": "Hello, world!"},
"fr": {"greeting": "Bonjour le monde !"},
"es": {"greeting": "隆Hola Mundo!"},
}
@app.get("/")
async def read_root(accept_language: Optional[str] = Header(None)):
lang = accept_language[:2] if accept_language else "en"
if lang not in messages:
lang = "en"
return {"message": messages[lang]["greeting"]}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root_en():
response = client.get("/", headers={"Accept-Language": "en-US"})
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
def test_read_root_fr():
response = client.get("/", headers={"Accept-Language": "fr-FR"})
assert response.status_code == 200
assert response.json() == {"message": "Bonjour le monde !"}
def test_read_root_es():
response = client.get("/", headers={"Accept-Language": "es-ES"})
assert response.status_code == 200
assert response.json() == {"message": "隆Hola Mundo!"}
def test_read_root_default():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
Ten przyk艂ad ustawia nag艂贸wek Accept-Language, aby okre艣li膰 偶膮dany j臋zyk. API zwraca powitanie w okre艣lonym j臋zyku. Testowanie zapewnia, 偶e API poprawnie obs艂uguje r贸偶ne preferencje j臋zykowe. Je艣li nag艂贸wek Accept-Language jest nieobecny, u偶ywany jest domy艣lny j臋zyk "en".
Wniosek
Testowanie jest niezb臋dn膮 cz臋艣ci膮 budowania solidnych i niezawodnych aplikacji FastAPI. TestClient zapewnia prosty i wygodny spos贸b testowania punkt贸w ko艅cowych API. Post臋puj膮c zgodnie z najlepszymi praktykami opisanymi w tym przewodniku, mo偶esz pisa膰 kompleksowe testy, kt贸re zapewniaj膮 jako艣膰 i stabilno艣膰 Twoich API. Od podstawowych 偶膮da艅 po zaawansowane techniki, takie jak wstrzykiwanie zale偶no艣ci i testowanie asynchroniczne, TestClient umo偶liwia tworzenie dobrze przetestowanego i 艂atwego w utrzymaniu kodu. Zaakceptuj testowanie jako podstawow膮 cz臋艣膰 swojego przep艂ywu pracy programistycznej, a zbudujesz API, kt贸re s膮 zar贸wno pot臋偶ne, jak i niezawodne dla u偶ytkownik贸w na ca艂ym 艣wiecie. Pami臋taj o znaczeniu integracji CI/CD w celu automatyzacji testowania i zapewnienia ci膮g艂ego zapewniania jako艣ci.